#
# openlookup/client/singleton.py
#
# Copyright 2009 Helsinki Institute for Information Technology
# and the authors.
#
# Authors: Ken Rimey <rimey@hiit.fi>
#

# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

from hashlib import sha1
from urllib2 import urlopen, Request, HTTPError
from urlparse import urljoin

from openlookup.lib.sexp import decode
from openlookup.engine.item import create_singleton_item, decode_item
from openlookup.engine.singleton import MAX_MAXVALS
from openlookup.client.base import BaseProxy

class Error(IOError): pass

class Proxy(BaseProxy):
    def __init__(self, url=None, ttl=None):
        BaseProxy.__init__(self, url)
        self.ttl = ttl

    def put(self, label, value, identity=None, ttl=None):
        """
        Set the value for a label.
        """
        ttl = ttl or self.ttl
        item = create_singleton_item(label, value, identity, ttl)
        self.put_item(item)

    def rm(self, label, identity=None, ttl=None):
        """
        Clear the value for a label.
        """
        ttl = ttl or self.ttl
        item = create_singleton_item(label, None, identity, ttl)
        self.put_item(item)

    def get(self, label, principal=None):
        """
        Get the value for a label.
        """
        item = self.get_item(label, principal)
        if item is not None:
            return item.decrypt_value(label)
        else:
            return None

    def query(self, label, principal='', maxvals=MAX_MAXVALS):
        """
        Return some values for a label.

        The optional parameter "principal" can be used to specify a
        prefix of 0-40 hexadecimal digits to narrow the query to a
        subset of the principals.  An empty prefix (the default) also
        matches an unsigned entry.

        Distinct values are returned in a deterministic pseudo-random
        circular order, starting from a randomly chosen value (but not
        guaranteeing that all values are equally likely to be chosen).
        """
        return [item.decrypt_value(label)
                for item in self.query_items(label, principal, maxvals)]

    def put_item(self, item):
        """
        Store an item.

        If the item is not signed, the expiration time is adjusted if
        necessary to supersede a preexisting item.
        """
        data = item.encode()
        headers = {'Content-type': 'application/octet-stream'}
        response = self.issue_request('/sexp', data, headers)
        assert response == ''

    def get_item(self, label, principal=None):
        """
        Look up an item.

        Note that the returned item can be a tombstone.  If no item is
        found, None is returned.
        """
        path = '/sexp?L=' + sha1(label).hexdigest()
        if principal:
            assert len(principal) == 40
            path += '&P=' + principal

        try:
            sexp = self.issue_request(path)
            item = decode_item(sexp)
            item.validate()
            return item
        except HTTPError, e:
            if e.code == 404:
                return None
            else:
                raise

    def query_items(self, label, principal='', maxvals=MAX_MAXVALS):
        """
        Return some non-tombstone items for a label.

        The optional parameter "principal" can be used to specify a
        prefix of 0-40 hexadecimal digits to narrow the query to a
        subset of the principals.  An empty prefix (the default) also
        matches an unsigned entry.

        Distinct items are returned in a deterministic pseudo-random
        circular order, starting from a randomly chosen item (but not
        guaranteeing that all items are equally likely to be chosen).
        """
        return list(self.gen_items(label, principal, maxvals))

    def gen_items(self, label, principal='', maxvals=MAX_MAXVALS):
        label_hash = sha1(label).hexdigest()
        path = '/sexp?L=%s&P=%s&N=%d' % (label_hash, principal, maxvals)
        sexp = self.issue_request(path)
        for x in decode(sexp):
            item = decode_item(x)
            item.validate()
            yield item

    def issue_request(self, path, data=None, headers={}, tries=5):
        while True:
            url = self.url
            if not url:
                raise Error('no gateway')

            url = urljoin(url, path)
            request = Request(url, data, headers)
            try:
                return urlopen(request).read()
            except HTTPError:
                raise
            except Exception:
                self.reset()
                tries -= 1
                if tries <= 0:
                    raise
